import entity
import pygame
import math
import random
import numpy as np
import xml.etree.cElementTree as ET
from ships.statuseffects import StatusEffects
from constants import DEBUG
import helpers

class Ship(entity.Entity):
    
    # Any effects that may affect the ship
    status_effects = None
    
    # Current target that the ship is attacking
    target = None
    
    # Current heading of the ship. Should be a unit vector pointing in the same
    # direction as the velocity.
    heading = np.array( [0,1], np.float )

    # How far has the ship traveled since creation. Used in determining target
    # priorities for towers.
    distance = 0
    
    # Evaluated at each update to check the state of the shhip
    # and react accordingly.
    # 0 => Alive, 1 => Killed by tower, 2 => Reached end of maze
    dead = 0
    
    # Used in calculating force when attacking. Different attack types will use
    # use this field in different ways to keep track of its next action.
    # Ex: Attackrun will change between seek and wander.
    attack_mode = 1

    # Time before our weapon can fire again
    cooldown = 0

    # Test
    wander_target = [0,1]
    aim_point = [0,0]
    def __init__(self, ship_name, start_position):
        entity.Entity.__init__(self)
        self.load_ship_data( ship_name )
        
        # TODO: Not yet working as intended...
        self.status_effects = StatusEffects()
        
        # Load image and keep a copy of the original. Only the original
        # image is transformed, when needed.
        self.orig_image, self.orig_rect = helpers.load_image(self.image_name,-1)
        self.image = self.orig_image
        self.rect = self.orig_rect        
    
        # Initial position and velocity
        self.position = np.array( start_position, np.float )
        self.velocity = np.zeros( 2, np.float )
    
    def update(self, dtime, structure_list):
        '''
        Called on each tick. Deals with any calculations that are needed.
        '''
        assert type(self.position).__name__ == "ndarray", \
               "Position is of incorrect type: {}.".format(
               type(self.position).__name__ )
        assert self.position.dtype.type is np.float_, \
               "Position is of incorrect type: {}.".format(
               self.position.dtype.type )
        
        self.status_effects.update(self)
        self.attack( dtime, structure_list )
        self.move(dtime)
        
        
    def attack(self, dtime, structure_list):
        '''
        Searches for a new target if we don't have one. Removes health from
        our current target if we are within range.
        '''
        self.cooldown -= dtime
        
        # Find a new target if we don't have one
        # TODO: Target priority
        if not self.target:
            self.target = self.find_target(structure_list)
            
        if self.cooldown <= 0:
            target_distance = np.linalg.norm(self.position-self.target.position)
            if self.radius <= target_distance:
                self.target.health -= self.damage
                self.cooldown = self.firerate
                if self.target.health <= 0:
                    self.target.kill()
                    self.target = None 
        
    def find_target(self, structure_list):
        smallest_distance = 1000000
        target = None
        
        for structure in structure_list:
            dist = np.linalg.norm( self.position - structure.position  )
            if dist < smallest_distance:
                smallest_distance = dist
                target = structure
        return target

        
    def draw(self, view_port):
        self.rect.centerx = round(self.position[0] + view_port.topleft[0])
        self.rect.centery = round(self.position[1] + view_port.topleft[1])
        
        view_port.screen.blit(self.image, self.rect)
        
        self.draw_healthbar(view_port)
        
        if DEBUG:
            if self.target:
                pygame.draw.line(view_port.screen,
                   (255,0,0), (self.rect.centerx, self.rect.centery),
                   (self.target.position[0], self.target.position[1]), 1 )
            #pygame.draw.line(view_port.screen,
            #    (0,255,0), (self.rect.centerx, self.rect.centery),
            #    (self.target2[0], self.target2[1]), 1 )
            #pygame.draw.line(view_port.screen,
            #    (255,255,0), (self.rect.centerx, self.rect.centery),
            #    (self.aim_point[0], self.aim_point[1]), 1 )
    
    def move3(self,time):
        dist = math.fabs( self.position[0] - self.target[0] ) \
            + math.fabs( self.position[1] - self.target[1] )        

        force = self.seek(self.target)
        
        acc_x = force[0] / self.mass
        acc_y = force[1] / self.mass
        
        vel_x = self.velocity[0] + acc_x*time
        vel_y = self.velocity[1] + acc_y*time
        
        pos_x = self.position[0] + vel_x*time
        pos_y = self.position[1] + vel_y*time
        
        self.velocity = vel_x, vel_y
        self.position = pos_x, pos_y
        
        self.rect.centerx = pos_x
        self.rect.centery = pos_y  

        angle = math.degrees( math.atan2(vel_x, vel_y) )        
        self.image = pygame.transform.rotate(self.orig_image, angle+180)
        
        self.distance += self.speed*time    
    
    def move(self, dt):
        '''
        Calculates the new position for the ship
        '''  
        if not self.target:
            force = self.wander(dt)
        elif self.attack_type == "attackrun":
            dist = np.linalg.norm( self.position - self.target.position )
            if self.attack_mode == 1:
                if dist < 50:
                    self.attack_mode = 2
                force = self.seek(self.target)
            elif self.attack_mode == 2:
                if dist > self.radius*2:
                    self.attack_mode = 1
                force = self.wander(dt)
        elif self.attack_type == "orbit":
            force = self.orbit( self.target )    
        elif self.attack_type == "bombard":
            dir = self.position - self.target.position
            dir /= np.linalg.norm( dir )
            # The last part is to ensure that we stop within firing range.
            stop_pos = self.target.position + dir*self.radius*0.9
            force = self.arrive( stop_pos, 2 )
        else:
            force = self.seek( self.target )
        
        #if not np.sum( force ):
        #    self.velocity = self.velocity * 0.8
        
        # Acceleration = Force / Mass
        self.update_position( dt, force )        

        heading = self.velocity / np.linalg.norm( self.velocity )        
        angle = math.degrees( math.atan2( heading[0], heading[1] ) )
        self.image = pygame.transform.rotate(self.orig_image, angle+180)
        
        self.distance += np.linalg.norm( self.velocity )*dt
        
    def seek(self,target):
        '''
        Calculate force needed for seeking after a target
        '''
        dV = target.position - self.position
        
        self.max_turn_rate = math.pi/10.0        
        
        norm = np.linalg.norm( dV )
        #if norm < 200:
        #    return np.zeros(2)
        dV /= norm        
        dV *= self.max_speed
        force = dV - self.velocity
        return force
    
    def flee(self,target):
        '''
        Calculate force needed to flee from a target.
        '''
        dV = self.position - target.position
        
        flee_distance = 200 * 200
        if dV[0]*dV[0]+dV[1]*dV[1] > flee_distance:
            return np.array( [0,0], np.float )
        
        dV /= np.linalg.norm( dV )
        dV *= self.base_speed
        force = dV - self.velocity
        return force
    
    def arrive(self, target, deceleration):
        dP = target.position - self.position
        distance = np.linalg.norm( dP )
        if distance > 0:
            speed = distance / deceleration
            if speed > self.speed:
                speed = self.speed
            dV = dP * speed / distance
            force = dV - self.velocity
            return force
        return np.zeros(2)
    
    def wander(self, dt):
        wander_radius = 5
        wander_distance = 5
        wander_jitter = 200*dt
        
        r1 = wander_jitter*random.uniform(-1,1)
        r2 = wander_jitter*random.uniform(-1,1)
        self.wander_target += np.array( [r1,r2] )
        self.wander_target /= np.linalg.norm( self.wander_target )
        self.wander_target *= wander_radius
        
        local_target = self.wander_target + [wander_distance,0]

        x = self.position[0] + self.heading[0]*local_target[0] - self.heading[1]*local_target[1]
        y = self.position[1] + self.heading[1]*local_target[0] + self.heading[0]*local_target[1]
        
        world_target = np.array( [x,y] )
        force = world_target - self.position
        return force
        
        
    def orbit(self, target):
        '''
        Finds the tangent to a circle. It then seeks towards this point.
        Returns zero force if the target is inside the circle.
        '''
        r = self.radius
        
        diff = self.position - target.position
        d = np.linalg.norm( diff )
        
        if d < r:
            return np.zeros(2)
        
        alpha = math.asin( r/d ) 
        beta = math.atan2( diff[1] , diff[0])
        
        theta = beta + alpha + math.pi
        l = math.sqrt( d*d - r*r )
        target_vec = np.array([math.cos(theta), math.sin(theta)])
        self.target2.position = target_vec * l + self.position 
        
        return self.seek( self.target2 )
        
    def reduce_speed(self, speed_reduction, time):
        new_speed = self.speed*speed_reduction
        if new_speed <= self.speed:
            self.speed = speed_reduction
            self.speed_time = time
    
    def addDebuff(self, tower, type):
        if type == "dot":
            self.status_effects.add_dot(tower)
            #self.abilities.debuffDot( tower )
        elif type == "speed":
            self.status_effects.add_speed_modifier(tower)
        
    def load_ship_data(self, file_name):
        '''
        Loads the ship data from the corresponding XML-file.
        '''

        # TODO: Hardcoded path to XML-files.
        file_name = '../data/ships/' + file_name + ".xml"
        tree = ET.ElementTree(file=file_name)
        
        self.name        = tree.find('name').text
        self.type        = tree.find('type').text
        self.description = tree.find('description').text
        self.health      = float( tree.find('health').text )
        self.speed       = float( tree.find('speed').text )
        self.max_force   = float( tree.find('force').text )
        self.mass        = float( tree.find('mass').text )
        self.turnrate    = float( tree.find('turnrate').text )
        self.radius      = float( tree.find('radius').text )
        self.damage      = float( tree.find('damage').text )
        self.firerate    = float( tree.find('firerate').text )
        self.image_name  = tree.find('image').text
        self.attack_type = tree.find('attack_type').text
        
        # Make a copy of some of the variables that can be changed
        self.max_health   = self.health
        self.max_speed    = self.speed
        self.max_turnrate = self.turnrate